はじめに
私はこれまで2年ほどLaravel関係で業務を行ってきました
そこで行っているベーシックな開発を通して安全なAPIを開発する方法を学んできました
そこで今回は実際に私がLaravelでAPIを作っていくうえで心掛けていることをまとめていきたいと思います
基礎的な内容も多く含まれるかと思いますがご覧いただけますと幸いです
LaravelAPIの基本の形
LaravelでのAPIは基本的に
-
api.phpにルーティングを記述 - ルーティングに紐づくControllerクラスに受け入れ口を記述
- Serviceクラスにロジックを記述
という構造になっています(厳密にはサービスクラスを使わずにController内へ処理を書くこともできますが、Controllerの責務が大きくなりすぎないよう処理をServiceクラスに記述しています)
ではこれをどう記述することで安全性が高く管理もしやすい構造を作成できるでしょうか
今回はこういった工夫の面についてお話していきます
そもそもAPI開発において大事なこととは?
私が大事だと思うことは通信の確実性と例外的事象への対応です
例えばパラメータを送信したときに受け取り手との型の齟齬があった際にそのまま通ってしまうとどうなるでしょうか
何も対策をしないと「何かわからないが失敗」「例外的な値がDBに登録されてしまう」などといった事象が発生してしまいます
もちろんAPI仕様書などを用意することで、送信側と受信側の認識違いを減らすことはできます。
しかし問題が起きた場合に備えて、原因を追いやすくすることも重要です
だからこそ私は、以下のような実装を行うことで、想定外の値が処理に入り込まないようにし、問題が発生した場合でも原因を追いやすくすることを意識しています
バリデーション
データの受け入れはコントローラからサービスに流れるように作成され、送信データはコントローラがそれを分解しサービスに分けるように作成されます
その際使用されるのがRequestクラスです
このリクエストクラスはフロントエンド側の処理で送信されたデータの塊を保持しています
例えば date = "2025-01-01" のようなものや id = 1のようなものが該当します
しかしもし date = "こんにちは" のように予期しないものが入ってしまうとどうなるでしょうか
日付のつもりで処理しようとしているものは文字列を捌くことはできません
そのためにバリデーションが必要です
バリデーションとは予期しない形での送信を防ぐものです
LaravelではController内でバリデーションを書くこともできますが、FormRequestクラスを使うことで、バリデーション処理をControllerから分離できます
このFormRequestクラスは元々使用していたRequestクラスに変わる形で配備し、そこに記述したバリデーションを通過するもののみを通過するようにする仕組みです
※自分が作成したものの一例
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreTransactionRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'occurred_on' => ['required', 'date'],
'type' => ['required', 'in:income,expense'],
'category_id' => ['required', 'exists:categories,id'],
'amount' => ['required', 'integer', 'min:1'],
'memo' => ['nullable', 'string', 'max:255'],
];
}
}
ここでは
'パラメータ名' => [各種条件]
という形でパラメータの設定を行うことができます
条件はそのパラメータが必須かやそのパラメータがどの型であることを許容するか、またその型についての制約を設定することができます
required => パラメータ必須
nullable => nullを許可する
integer => 整数型
date => 日付型
string => 文字列型
今回使用しているものはこのようなものです
他にも種類があるので自分でカスタムして最適なバリデーションを設定することができます
さらにカスタムできるロジック込みのルールを設定することもできます
エラーハンドリングとログ出力について
例外発生時その例外内容の特定や表示するための情報を返す必要があります
しかしその面においても気を付けておくべきことがあります
それはレスポンス形式の統一です
エラー形式をJSON形式で統一することでより扱いやすくすることができます
フロントエンドでもより処理しやすい形式はJSONであることが多くすべてJSONで返すことで受け取り手もわかりやすくすることができます
また例外メッセージには、SQL文、テーブル名、カラム名、ファイルパスなど、外部に公開すべきでない情報が含まれる可能性があります
そのためエラーが発生した際にレスポンスに例外を含んではいけません
その情報には機密な情報が含まれていたり受け取り手にはわからない形式であることも多いです
そこで自分の見たい情報はLogクラスに別途で分けて記録することをお勧めします
開発者が確認したい詳細な情報は Log::error() に記録し、ユーザーやフロントエンドに返す内容は response()->json() で汎用的なメッセージにすることで、内部情報を公開せずに原因調査もしやすい形にできます。
※このように使い分けることで実際の例外と表示されるものを分けることができる
Log::error('処理中にエラーが発生しました', [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
return response()->json([
'message' => '処理に失敗しました。'
], 500);
認証・認可を意識する
先ほどのバリデーションによって型そのものから違うものは防ぐことができます
しかし数値なら通るということは悪意を持って別の人のIDを差し込むことはできてしまうわけです
そのためuser_idをそのまま使用するのは危険です
そこで認証ミドルウェアを使用します
各種設定を施したうえでミドルウェアを通過する箇所を使用すると $request->user()という形でログインユーザーを判定できます
リクエストで user_id を送信されても、その値を使わず、認証情報から取得したユーザーIDを利用することで、他人のIDを指定されるリスクを減らせます
※このように認証済みユーザーの情報を取得することで、リクエストで送信された user_id に頼らず処理できます。
return response()->json([
'data' => [
'id' => $request->user()->id,
'login_id' => $request->user()->login_id,
],
]);
ただし、$request->user() を利用するには、対象のルートに auth:sanctum などの認証ミドルウェアを設定しておく必要があります
また、認証済みユーザーを取得できたとしても、更新や削除などで操作対象のデータが本当にそのユーザーのものかを確認する必要があります
このように「誰がリクエストしているか」を確認するのが認証であり、「そのユーザーが対象データを操作してよいか」を確認するのが認可です
そのため、IDだけでデータを取得するのではなく、認証情報から取得したユーザーIDも条件に含めて取得することを意識しています
$item = Item::where('id', $id)
->where('user_id', $request->user()->id)
->firstOrFail();
このようにすることで、指定されたデータが存在するだけでなく、ログイン中ユーザーに紐づくデータであることも確認できます
まとめ
いかがでしたでしょうか
このような方法を使用することで比較的安全にLaravelのAPIを作成できます
自分もエンジニアとしてまだまだなところもありますがこれからも学習を進めてより良いAPIが開発できるように学びを進めていこうと思います